package me.skyun.androidarch.connect;

import android.bluetooth.BluetoothSocket;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.google.gson.JsonSyntaxException;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.List;

import me.skyun.androidarch.common.DataInputStreamEx;
import me.skyun.androidarch.common.DataOutputStreamEx;
import me.skyun.androidarch.common.Utils;

/**
 * Created by skyun on 17/8/20.
 */
public class Connection extends Thread {

    public enum State {Connecting, Connected, Disconnected}

    public enum MetaType {Image, Message, Object, List}

    public interface Callback {
        void onLog(String log);

        void onStateChanged(Connection connection, State state);

        void onReceive(Connection connection, String msg, String arg);

        void onReceive(Connection connection, String name, Bitmap bitmap);

        void onReceiveObject(Connection connection, Object obj);

        void onReceive(Connection connection, List list);
    }

    public static class DefaultCallback implements Callback {
        @Override
        public void onLog(String log) {
        }

        @Override
        public void onStateChanged(Connection connection, State state) {
        }

        @Override
        public void onReceive(Connection connection, String msg, String arg) {
        }

        @Override
        public void onReceive(Connection connection, String name, Bitmap bitmap) {
        }

        @Override
        public void onReceiveObject(Connection connection, Object obj) {
        }

        @Override
        public void onReceive(Connection connection, List list) {
        }
    }

    private Closeable mSocket;

    private final Object mLock = new Object();
    private DataOutputStreamEx mDataOutputStream;
    private DataInputStreamEx mDataInputStream;

    private Callback mCallback = new DefaultCallback();

    public String getRemoteName() {
        if (mSocket instanceof BluetoothSocket) {
            return ((BluetoothSocket) mSocket).getRemoteDevice().getName();
        } else if (mSocket instanceof Socket) {
            SocketAddress socketAddress = ((Socket) mSocket).getLocalSocketAddress();
            if (socketAddress != null) {
                return socketAddress.toString();
            }
        }
        return "Unknown remote device";
    }

    public Connection(Closeable socket, final Callback callback) {
        mSocket = socket;
        mCallback = callback;
        try {
            if (socket instanceof BluetoothSocket) {
                mDataInputStream = new DataInputStreamEx(((BluetoothSocket) socket).getInputStream());
                mDataOutputStream = new DataOutputStreamEx(((BluetoothSocket) socket).getOutputStream());
            } else if (socket instanceof Socket) {
                mDataInputStream = new DataInputStreamEx(((Socket) socket).getInputStream());
                mDataOutputStream = new DataOutputStreamEx(((Socket) socket).getOutputStream());
            } else {
                throw new IllegalArgumentException("Unsupported socket type");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                callback.onStateChanged(Connection.this, State.Connected);
            }
        });
    }

    @Override
    public void run() {
        while (true) {
            try {
                MetaType metaType = mDataInputStream.readEnum(MetaType.class);
                if (metaType == null) {
                    disconnect();
                    break;
                }
                if (metaType == MetaType.Image) {
                    final String name = mDataInputStream.readString();
                    final Bitmap bitmap = mDataInputStream.readBitmap();
                    if (bitmap != null) {
                        log(name + " " + bitmap.getByteCount());
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                mCallback.onReceive(Connection.this, name, bitmap);
                            }
                        });
                    }
                    log("%s, %s", name, bitmap.getByteCount());
                } else if (metaType == MetaType.Object) {
                    final Object obj = mDataInputStream.readObject();
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            mCallback.onReceiveObject(Connection.this, obj);
                        }
                    });
                    log(obj.getClass().getName());
                } else if (metaType == MetaType.List) {
                    final List list = mDataInputStream.readList();
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            mCallback.onReceive(Connection.this, list);
                        }
                    });
                    log("<--[%s] %s, %s", metaType, list.get(0).getClass().getName(), list.size());
                } else if (metaType == MetaType.Message) {
                    final String msg = mDataInputStream.readString();
                    final String arg = mDataInputStream.readString();
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            mCallback.onReceive(Connection.this, msg, arg);
                        }
                    });
                    log(msg);
                }
            } catch (IOException e) {
                e.printStackTrace();
                disconnect();
                return;
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (JsonSyntaxException e) {
                e.printStackTrace();
            }
        }
    }

    public void sendList(final List list) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    try {
                        log("[%s]-->", MetaType.List);
                        mDataOutputStream.writeEnum(MetaType.List);
                        mDataOutputStream.writeList(list);
                        mDataOutputStream.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public void sendObject(final Object object) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    try {
                        log("[%s]-->", MetaType.Object);
                        mDataOutputStream.writeEnum(MetaType.Object);
                        mDataOutputStream.writeObject(object);
                        mDataOutputStream.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public void sendMessage(final String msg) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    sendMessage(msg, "");
                }
            }
        }).start();
    }

    public void sendMessage(final String msg, final String arg) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    log("[%s]--> %s", MetaType.Message, msg);
                    mDataOutputStream.writeEnum(MetaType.Message);
                    mDataOutputStream.writeString(msg);
                    mDataOutputStream.writeString(arg);
                    mDataOutputStream.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public void sendBitmap(final String name, final Bitmap bitmap) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mLock) {
                    try {
                        log("[%s]--> %s (%s)", MetaType.Image, name, "" + bitmap.getByteCount());
                        mDataOutputStream.writeEnum(MetaType.Image);
                        mDataOutputStream.writeString(name);
                        mDataOutputStream.writeBitmap(bitmap);
                        mDataOutputStream.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public void sendDrawable(final String name, final Drawable drawable) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = Utils.drawable2Bitmap(drawable);
                log("[%s]--> %s (%s)", MetaType.Image, name, "" + bitmap.getByteCount());
                sendBitmap(name, bitmap);
            }
        }).start();
    }

    private void log(String format, Object... args) {
        String log = String.format(format, args);
        Log.d(getClass().getName(), log);
        mCallback.onLog(log);
    }

    private void log(String log) {
        mCallback.onLog(log);
    }

    public void disconnect() {
        if (mSocket == null) {
            return;
        }
        try {
            mSocket.close();
            mSocket = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                mCallback.onStateChanged(Connection.this, State.Disconnected);
            }
        });
    }
}
